package per.cy.common.util;

import cn.hutool.log.Log;
import cn.hutool.log.LogFactory;
import com.jcraft.jsch.*;
import lombok.Getter;
import lombok.Setter;

import java.io.File;
import java.lang.reflect.Field;
import java.util.Objects;
import java.util.Properties;
import java.util.Vector;

/**
 * @author cy
 * <p>
 * date 2024/8/20
 * <p>
 * SftpTools类用于处理SFTP（Secure File Transfer Protocol）连接和文件传输
 * 它提供了连接到远程服务器的功能，并执行常见的文件操作，如上传、下载、删除文件等
 */
public class SftpTools {

    // 日志记录器
    private static final Log log = LogFactory.get();

    // SFTP服务器的主机名
    @Setter
    private String host;
    // SFTP服务器的用户名
    @Setter
    private String username;
    // SFTP服务器的密码
    @Setter
    private String password;
    // 私钥文件路径，用于密钥认证
    @Setter
    private String privateKeyPath;
    // 字符编码
    @Setter
    private String charset;
    // SFTP服务器的端口号
    @Getter
    private static final int SFTP_PORT = 22;

    // JSch会话对象
    private Session session = null;
    // SFTP通道对象
    private ChannelSftp sftp = null;

    /**
     * 默认构造函数
     */
    public SftpTools() {
    }

    /**
     * 建立SFTP连接
     * 该方法配置JSch对象，创建并初始化Session对象，然后打开SFTP通道
     */
    public void connect() {
        JSch jSch = new JSch();

        // 如果提供了私钥路径，添加私钥
        if (privateKeyPath != null) {
            try {
                jSch.addIdentity(privateKeyPath);
            } catch (Exception e) {
                if (log.isErrorEnabled()) {
                    log.error("Failed to add identity: " + e.getMessage(), e);
                }
                throw new RuntimeException("Failed to add identity: " + e.getMessage(), e);
            }
        }

        // 创建会话
        try {
            session = jSch.getSession(username, host, SFTP_PORT);
            if (log.isInfoEnabled()) {
                log.info("session created .");
            }
        } catch (Exception e) {
            if (log.isErrorEnabled()) {
                log.error("Failed to connect: " + e.getMessage(), e);
            }
            throw new RuntimeException("Failed to connect: " + e.getMessage(), e);
        }

        // 设置密码，如果提供的话
        if (password != null) {
            session.setPassword(password);
        }

        // 配置会话属性，禁用StrictHostKeyChecking
        Properties p = new Properties();
        p.setProperty("StrictHostKeyChecking", "no");
        session.setConfig(p);

        // 建立会话连接
        try {
            session.connect();
            if (log.isInfoEnabled()) {
                log.info("session connect to {}@{} .", username, host);
            }
        } catch (Exception e) {
            if (log.isErrorEnabled()) {
                log.error("Failed to connect: " + e.getMessage(), e);
            }
            throw new RuntimeException("Failed to connect: " + e.getMessage(), e);
        }

        // 打开SFTP通道
        try {
            Channel channel = session.openChannel("sftp");
            channel.connect();
            if (log.isInfoEnabled()) {
                log.info("channel connect to {}@{} .", username, host);
            }
            sftp = (ChannelSftp) channel;
        } catch (JSchException e) {
            if (log.isErrorEnabled()) {
                log.error("Failed to open channel: " + e.getMessage(), e);
            }
            throw new RuntimeException("Failed to open channel: " + e.getMessage(), e);
        }

        // 设置SFTP服务器版本和文件名编码
        if (charset != null) {
            try {
                Field server_version = this.sftp.getClass().getDeclaredField("server_version");
                server_version.setAccessible(true);
                server_version.set(this.sftp, 2);
                sftp.setFilenameEncoding(charset);
            } catch (NoSuchFieldException | IllegalAccessException e) {
                if (log.isErrorEnabled()) {
                    log.error("Failed to set server version: " + e.getMessage(), e);
                }
                throw new RuntimeException("Failed to set server version: " + e.getMessage(), e);
            } catch (SftpException e) {
                if (log.isErrorEnabled()) {
                    log.error("Failed to set filename encoding: " + e.getMessage(), e);
                }
                throw new RuntimeException("Failed to set filename encoding: " + e.getMessage(), e);
            }
        }

        // 日志记录连接成功
        log.info("SFTP connection established. {}@{}", username, host);
    }

    /**
     * 断开SFTP连接此方法首先断开与SFTP服务器的连接，然后断开与会话的连接
     */
    public void disconnect() {
        // 如果sftp对象不为空，则断开sftp连接
        if (sftp != null) {
            sftp.disconnect();
        }

        // 如果session对象不为空，则断开session连接
        if (session != null) {
            session.disconnect();
        }

        // 如果日志对象的Info级别已启用，则记录一条日志信息
        if (log.isInfoEnabled()) {
            log.info("SFTP connection closed.");
        }
    }

    /**
     * 通过SFTP上传文件到远程服务器
     *
     * @param remotePath 远程服务器上的目标路径
     * @param filePath   本地文件的路径
     */
    public void upload(String remotePath, String filePath) {
        try {
            File file = new File(filePath);
            if (!file.isFile()) {
                if (log.isInfoEnabled()) {
                    log.info("{} is not file.", filePath);
                }
                throw new RuntimeException(filePath + " is not file.");
            }
            // 切换工作目录到指定的远程路径
            sftp.cd(remotePath);
            // 上传本地文件到远程服务器，参数分别为本地文件路径和远程文件名
            sftp.put(filePath, file.getName());
            if (log.isInfoEnabled()) {
                log.info("File uploaded successfully. {} to {}", filePath, remotePath);
            }
        } catch (SftpException e) {
            // 日志记录上传失败的错误信息
            if (log.isErrorEnabled()) {
                log.error("Failed to upload file: " + e.getMessage(), e);
            }
            // 抛出运行时异常，通知调用者上传失败
            throw new RuntimeException("Failed to upload file: " + e.getMessage(), e);
        }
    }

    /**
     * 递归上传目录到远程服务器
     *
     * @param remotePath 远程服务器上的路径
     * @param filePath   本地文件系统中的路径
     * @throws RuntimeException 如果文件不存在或不是目录，或者上传过程中出现错误
     */
    public void uploadDir(String remotePath, String filePath) {
        try {
            // 切换到远程服务器上的指定目录
            sftp.cd(remotePath);

            // 创建File对象，用于操作本地文件
            File file = new File(filePath);

            // 检查文件是否存在
            if (!file.exists()) {
                // 如果文件不存在，记录日志并抛出异常
                if (log.isInfoEnabled()) {
                    log.info("{} is not exist", filePath);
                }
                throw new RuntimeException(filePath + " is not exist");
            }

            // 检查是否为目录
            if (!file.isDirectory()) {
                // 如果不是目录，记录日志并抛出异常
                if (log.isInfoEnabled()) {
                    log.info("{} is not Directory", filePath);
                }
                throw new RuntimeException(filePath + " is not Directory");
            }

            // 遍历目录下的文件和子目录
            for (File f : Objects.requireNonNull(file.listFiles())) {
                if (f.isDirectory()) {
                    mkdirDir(f.getName());
                    // 如果是目录，则递归调用uploadDir方法上传子目录
                    uploadDir(remotePath + "/" + f.getName(), f.getPath());
                } else {
                    // 如果是文件，则调用upload方法上传文件
                    upload(remotePath, f.getPath());
                }
            }
        } catch (SftpException e) {
            // 捕获SftpException异常，记录错误日志并抛出RuntimeException
            if (log.isErrorEnabled()) {
                log.error("Failed to upload file: " + e.getMessage(), e);
            }
            throw new RuntimeException("Failed to upload file: " + e.getMessage(), e);
        }
    }

    /**
     * 在远程服务器上创建目录，如果目录已存在，则不进行创建
     *
     * @param remotePath 远程目录路径
     */
    public void mkdirDir(String remotePath) {
        // 检查远程目录是否已经存在
        try {
            sftp.stat(remotePath);
        } catch (SftpException e) {
            // 如果目录不存在，则尝试创建目录
            try {
                sftp.mkdir(remotePath);
                // 日志记录目录创建成功
                if (log.isInfoEnabled()) {
                    log.info("Directory created successfully. {}", remotePath);
                }
            } catch (SftpException sftpException) {
                // 处理创建目录失败的情况
                if (log.isErrorEnabled()) {
                    log.error("Failed to create directory: " + e.getMessage(), e);
                }
                // 抛出运行时异常，提示创建目录失败
                throw new RuntimeException("Failed to create directory: " + e.getMessage(), e);
            }
        }
    }

    /**
     * 从远程服务器下载文件到本地
     *
     * @param remotePath 远程服务器上的文件路径
     * @param filePath   本地文件路径
     */
    public void download(String remotePath, String filePath) {
        try {
            // 下载远程文件到本地
            sftp.get(remotePath, filePath);
            // 记录下载成功日志
            if (log.isInfoEnabled()) {
                log.info("File downloaded successfully. {} to {}", remotePath, filePath);
            }
        } catch (SftpException e) {
            // 日志记录下载失败的错误信息
            if (log.isErrorEnabled()) {
                log.error("Failed to download file: " + e.getMessage(), e);
            }
            // 抛出运行时异常，通知调用者下载失败
            throw new RuntimeException("Failed to download file: " + e.getMessage(), e);
        }
    }

    /**
     * 递归下载远程服务器上的目录
     *
     * @param remotePath 远程服务器上的目录路径
     * @param filePath   本地要保存下载文件的目录路径
     */
    public void downloadDir(String remotePath, String filePath) {
        try {
            // 列出远程路径下的所有文件和目录
            Vector<ChannelSftp.LsEntry> entries = sftp.ls(remotePath);
            for (ChannelSftp.LsEntry entry : entries) {
                String entryName = entry.getFilename();
                // 忽略当前目录和上一级目录
                if (".".equals(entryName) || "..".equals(entryName)) {
                    continue;
                }

                String separator = File.separator;

                // 如果是目录，则递归处理
                if (entry.getAttrs().isDir()) {
                    File localFile = new File(filePath + separator + entryName);
                    // 如果本地目录不存在，则尝试创建
                    if (!localFile.exists()) {
                        boolean mkdir = localFile.mkdir();
                        // 创建目录失败，记录错误日志并抛出异常
                        if (!mkdir) {
                            if (log.isErrorEnabled()) {
                                log.error("Failed to create directory: {}", localFile.getAbsolutePath());
                            }
                            throw new RuntimeException("Failed to create directory: " + localFile.getAbsolutePath());
                        } else if (log.isInfoEnabled()) {
                            log.info("Directory created successfully. {}", localFile.getAbsolutePath());
                        }
                    }
                    // 递归下载子目录
                    downloadDir(remotePath + separator + entryName, localFile.getAbsolutePath());
                } else {
                    // 如果是文件，则进行下载
                    if (log.isInfoEnabled()) {
                        log.info("found file {} at {}", entryName, remotePath);
                    }
                    download(remotePath + separator + entryName, filePath + separator + entryName);
                }
            }
        } catch (SftpException e) {
            // 下载过程中发生异常，记录错误日志并抛出运行时异常
            if (log.isErrorEnabled()) {
                log.error("Failed to download file: " + e.getMessage(), e);
            }
            throw new RuntimeException("Failed to download file: " + e.getMessage(), e);
        }
    }
}
